﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using Phoenix.ActionResults;
using Phoenix.Diagnostics;
using Phoenix.Exceptions;
using Phoenix.Filters;
using Phoenix.Filters.ActionFilters;
using Phoenix.Filters.ExceptionFilters;
using Phoenix.Filters.ResultFilters;

namespace Phoenix.Framework
{
    internal class ActionExecution
    {
        readonly IErrorReporter errorReporter;
        readonly IPhoenixHost host;
        readonly IControllerLocator controllerLocator;
        readonly IControllerFactory controllerFactory;
        Filter[] filters;
        ControllerContext controllerContext;
        TaskCompletionSource<ActionResult> executionCompletionSource;

        public ActionExecution(
            InvokeActionRequest invokeActionRequest,
            ControllerActionContext newContext,
            IErrorReporter errorReporter,
            IPhoenixHost host,
            IControllerLocator controllerLocator,
            IControllerFactory controllerFactory)
        {
            InvokeActionRequest = invokeActionRequest;
            NewContext = newContext;
            this.errorReporter = errorReporter;
            this.host = host;
            this.controllerLocator = controllerLocator;
            this.controllerFactory = controllerFactory;

        }
        public Task ExecutionTask { get; private set; }
        public ControllerActionContext NewContext { get; private set; }
        public InvokeActionRequest InvokeActionRequest { get; private set; }

        public void Execute(Dispatcher appDispatcher, TaskScheduler uiScheduler)
        {
            Task initialTask = null;

            if (!appDispatcher.CheckAccess())
                initialTask = Task.Factory.StartNew(() => InvokeActionRequest.TargetFrame.BeginOperation(InvokeActionRequest), CancellationToken.None,
                    TaskCreationOptions.None, uiScheduler);
            else
                InvokeActionRequest.TargetFrame.BeginOperation(InvokeActionRequest);

            Trace.WriteInfo(this, () => string.Format("Executing {0}", InvokeActionRequest.Target));

            executionCompletionSource = new TaskCompletionSource<ActionResult>();

            Task<ControllerContext> resolveTask;
            if (initialTask == null)
                resolveTask = Task.Factory.StartNew(() => ResolveControllerContext(), CancellationToken.None,
                    TaskCreationOptions.None, TaskScheduler.Default);
            else
            {
                resolveTask = initialTask.ContinueWith(t =>
                {
                    t.Wait();
                    return ResolveControllerContext();
                });
            }

            resolveTask
                .ContinueWith(t =>
                {
                    if (t.IsFaulted)
                        filters = FilterProviders.Providers.GetFilters(null, null).ToArray();

                    controllerContext = t.Result;
                    var controllerDescriptor = controllerContext.ControllerDescriptor;
                    var actionDescriptor = controllerDescriptor.FindAction(NewContext);
                    filters = FilterProviders.Providers.GetFilters(controllerContext, actionDescriptor).ToArray();

                    ExecuteControllerAction(actionDescriptor);
                })
                .ContinueWith(t =>
                {
                    if (t.IsCanceled)
                        executionCompletionSource.SetCanceled();
                    else if (t.IsFaulted && t.Exception != null)
                        executionCompletionSource.SetException(t.Exception);
                });

            ExecutionTask = executionCompletionSource.Task
                .ContinueWith(t =>
                    {
                        if (t.Exception != null)
                        {
                            var ex = t.Exception.InnerException;
                            var exceptionContext = new ExceptionContext(InvokeActionRequest.Target, ex);
                            RunErrorFilters(exceptionContext);
                            InvokeActionRequest.TargetFrame.EndOperation(InvokeActionRequest);

                            if (!exceptionContext.ExceptionHandled)
                            {
                                errorReporter.ReportException(ex, null, false);
                                t.Wait(); //Propogate exception
                            }
                        }
                        else
                        {
                            var actionResult = t.Result ?? new DoNothingActionResult(NewContext);
                            ExecuteControllerActionResultWithFilters(actionResult, InvokeActionRequest, NewContext);
                            InvokeActionRequest.TargetFrame.EndOperation(InvokeActionRequest);

                            var invokeCompletedSteps = actionResult as IHaveInvokeCompletedSteps;
                            if (invokeCompletedSteps != null)
                            {
                                HandleCompletionSteps(invokeCompletedSteps);
                            }
                        }
                    }, host.AppScheduler);
        }

        void ExecuteControllerAction(IActionDescriptor actionDescriptor)
        {
            var actionFilters = filters.Select(f => f.Instance).OfType<IActionFilter>().ToArray();

            var actionExecutingContext = new ActionExecutingContext(controllerContext);
            foreach (var actionFilter in actionFilters.TakeWhile(f => actionExecutingContext.Result == null))
            {
                actionFilter.OnActionExecuting(actionExecutingContext);
            }

            var actionResultTask = actionExecutingContext.Result != null
                                        ? Task.Factory.StartNew(() => actionExecutingContext.Result)
                                        : actionDescriptor.Execute(controllerContext.Controller, NewContext);

            actionResultTask
                .ContinueWith(task =>
                {
                    // If we have been cancelled, just propogate the cancellation
                    if (task.IsCanceled)
                        executionCompletionSource.SetCanceled();

                    ActionResult actionResult;
                    
                    //If an error occured while getting the action result, default to do nothing
                    if (task.IsFaulted && task.Exception != null)
                        actionResult = new DoNothingActionResult(NewContext);
                    else
                        actionResult = task.Result;

                    var executedContext = new ActionExecutedContext(controllerContext, task.Exception)
                        {
                            Result = actionResult
                        };

                    foreach (var actionFilter in actionFilters.TakeWhile(f => !executedContext.ExceptionHandled))
                    {
                        actionFilter.OnActionExecuted(executedContext);
                    }

                    
                    if (task.IsFaulted && task.Exception != null)
                    {
                        // If we still have a result, and the exception has been handled by a filter, continue on
                        if (executedContext.ExceptionHandled && executedContext.Result != null)
                            executionCompletionSource.SetResult(executedContext.Result);
                        else
                            executionCompletionSource.SetException(task.Exception);
                    }
                    else
                        executionCompletionSource.SetResult(executedContext.Result);
                });
        }

        ControllerContext ResolveControllerContext()
        {
            var controllerType = controllerLocator.Locate(NewContext.ControllerAction);
            var controller = controllerFactory.Create(controllerType, NewContext);

            var controllerDescriptor = new ReflectedControllerDescriptor(controllerType);
            return new ControllerContext(controllerDescriptor, controller, NewContext, NewContext.ControllerAction);
        }

        void HandleCompletionSteps(IHaveInvokeCompletedSteps invokeCompletedSteps)
        {
            try
            {
                invokeCompletedSteps.InvokeComplete();
            }
            catch (Exception ex)
            {
                var exceptionContext = new ExceptionContext(InvokeActionRequest.Target, ex);
                RunErrorFilters(exceptionContext);

                if (!exceptionContext.ExceptionHandled)
                {
                    errorReporter.ReportException(ex, null, false);
                    throw;
                }
            }
        }

        void ExecuteControllerActionResultWithFilters(ActionResult actionResult, InvokeActionRequest invokeActionRequest, ControllerActionContext newContext)
        {
            var resultFilters = filters.Select(f => f.Instance).OfType<IResultFilter>().ToArray();
            try
            {
                ExecutingResultFilters(actionResult, invokeActionRequest, newContext, resultFilters);
                try
                {
                    actionResult.Execute(invokeActionRequest.TargetFrame);
                }
                catch (Exception ex)
                {
                    var resultExecutedContext = new ResultExecutedContext(controllerContext, ex);
                    foreach (var filter in resultFilters.TakeWhile(f=>!resultExecutedContext.ExceptionHandled))
                    {
                        filter.OnResultExecuted(resultExecutedContext);
                    }
                    if (!resultExecutedContext.ExceptionHandled)
                    {
                        throw;
                    }
                    return;
                }
            }
            catch (Exception ex)
            {
                var message = string.Format("Controller Action {0} invocation failed", invokeActionRequest.Target);
                var invocationFailedException = new ControllerActionInvocationFailedException(message, ex);
                errorReporter.ReportException(ex, null, false);
                throw invocationFailedException;
            }

            var resultExecutedContextNoEx = new ResultExecutedContext(controllerContext, null);
            foreach (var filter in resultFilters)
            {
                filter.OnResultExecuted(resultExecutedContextNoEx);
            }
        }

        void ExecutingResultFilters(ActionResult actionResult, InvokeActionRequest invokeActionRequest, ControllerActionContext newContext, IEnumerable<IResultFilter> resultFilters)
        {
            var resultExecutingContext = new ResultExecutingContext(actionResult, invokeActionRequest.CurrentContext, newContext, controllerContext);
            foreach (var filter in resultFilters)
            {
                filter.OnResultExecuting(resultExecutingContext);
            }
        }

        void RunErrorFilters(ExceptionContext exceptionContext)
        {
            var exceptionFilters = filters.Select(f => f.Instance).OfType<IExceptionFilter>();
            foreach (var filter in exceptionFilters.TakeWhile(filter => !exceptionContext.ExceptionHandled))
            {
                filter.OnException(exceptionContext);
            }
        }

        /// <summary>
        /// We want to track the task that is returned from the ActionInvoker, hence updating the ExeuctionTask with the continuation
        /// </summary>
        /// <param name="continuation"></param>
        /// <returns></returns>
        public Task ContinueWith(Action<Task> continuation)
        {
            ExecutionTask = ExecutionTask.ContinueWith(continuation);
            return ExecutionTask;
        }

        /// <summary>
        /// We want to track the task that is returned from the ActionInvoker, hence updating the ExeuctionTask with the continuation
        /// </summary>
        /// <param name="continuation"></param>
        /// <param name="scheduler">Scheduler to use</param>
        /// <returns></returns>
        public Task ContinueWith(Action<Task> continuation, TaskScheduler scheduler)
        {
            ExecutionTask = ExecutionTask.ContinueWith(continuation, scheduler);
            return ExecutionTask;
        }
    }
}